/*
 * Flashlib - QB9 common Flash library
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.qb9.flashlib.fsm
{
	import com.qb9.flashlib.tasks.*;
	import com.qb9.flashlib.events.QEvent;
	import flash.events.*;

	/**
	 * Finite State Machine.
	 *
	 * <p>Main features:
	 * <ul>
	 * <li>Event based transitions.</li>
	 * <li>Support for both inline creation (see example below), and user-defined State classes.</li>
	 * <li>Support for hierarchical (nested) FSMs.</li>
	 * </ul>
	 * </p>
	 *
	 * <p>Usage example:
	 * <pre>
	 * 	// Define the FSM:
	 * 	var fsm:FSM = new FSM();
	 * 	fsm.registerState('A')
	 * 		.onEvent('gotoB', fsm).goto('B');
	 * 	fsm.registerState('B');
	 * 	
	 * 	// Fire-up the FSM:
	 * 	fsm.enter();
	 * 	assertEquals('A', fsm.state.name);
	 * 	
	 * 	// Dispatch an event that provokes a transition:
	 * 	fsm.dispatch('gotoB');
	 * 	assertEquals('B', fsm.state.name);
	 * 	
	 * 	// Done using the FSM:
	 * 	fsm.dispose();
	 * </pre>
	 * </p>
	 */
	public class FSM extends State
	{
		protected var states:Object;
		protected var current:IState;
		protected var stack:Array;

		/** Amount of states to remember for the State.PREVIOUS transition. */
		public var maxStackSize:uint;

		/**
		 * Create a FSM instance.
		 *
		 * @param maxStackSize Amount of states to remember for the State.PREVIOUS transition.
		 */
		public function FSM(maxStackSize:uint = 10)
		{
			this.maxStackSize = maxStackSize;
			states = {};
			stack = [];
		}

		/** Current state instance. */
		public function get state():IState
		{
			return current;
		}

		protected function get hasInitialState():Boolean
		{
			return states[State.INITIAL] != null;
		}

		protected function createInitialState(name:String):void
		{
			registerState(State.INITIAL).onEvent(StateEvent.ENTER).goto(name);
		}

		/**
		 * Register a state. The first state registered is the initial state.
		 *
		 * @param name Unique identifier for the new state.
		 * @param state <code>IState</code> instance. If not specified, a new <code>State</code> instance will be created.
		 * 
		 * @return The <code>IState</code> instance that was registered.
		 */
		public function registerState(name:String, state:IState = null):IState
		{
			if (state == null)
				state = new State();
			states[name] = state;
			state.name = name;
			if (! hasInitialState)
				createInitialState(name);
			return state;
		}

		protected function exitState(data:* = null):void
		{
			state.exit(data);
			state.stop();
			state.removeEventListener(TransitionEvent.TRANSITION, onTransitionRequest);
			current = null;
		}

		protected function enterState(name:String, data:* = null):void
		{
			current = getState(name);
			state.addEventListener(TransitionEvent.TRANSITION, onTransitionRequest);
			state.start();
			state.enter(data);
		}

		protected function updateStack(next:String):String
		{
			if (next == State.PREVIOUS)
				next = stack.pop();
			else
				stack.push(state.name);
			if (stack.length > maxStackSize)
				stack.shift();
			return next;
		}

		protected function goto(name:String, data:* = null):void
		{
			if (state && state.name == name) return;
			name = updateStack(name);
			exitState(data);
			enterState(name, data);
		}

		protected function onTransitionRequest(ev:TransitionEvent):void
		{
			if (ev.isUpperState())
				requestTransition(ev.dereferenceUpperState(), ev.data);
			else
				goto(ev.nextState, ev.data);
		}

		/** Stop the task and dispose all substates. */
		public override function dispose():void
		{
			super.dispose();
			for (var name:String in states)
			{
				getState(name).dispose();
				delete states[name];
			}
		}

		/** Return the <code>IState</code> instance with the given id. */
		public function getState(name:String):IState
		{
			if (!states[name])
				throw new Error('Unknown state: ' + name);
			return states[name];
		}
		
		/** @inheritDoc */
		public override function update(milliseconds:uint):void
		{
			var elapsed:uint = state ? updateSubtask(state, milliseconds) : milliseconds;
			super.update(elapsed);
		}
		
		/** @inheritDoc */
		public override function start():void
		{
			super.start();
			if (state) state.start();
		}

		/** @inheritDoc */
		public override function stop():void
		{
			if (state) state.stop();
			super.stop();
		}

		/** Call this function to start the FSM. */
		public override function enter(data:* = null):void
		{
			if (!running)
				start();
			super.enter(data);
			if (hasInitialState)
				enterState(State.INITIAL, data);
		}

		/** @inheritDoc */
		public override function exit(data:* = null):void
		{
			if (state)
				exitState(data);
			super.exit(data);
		}
	
		protected function createState(stateData:Object):IState
		{
			return stateData.nested ? new FSM() : new State();
		}

		/**
		 * Create and configure the states and transitions from a configuration object.
		 */
		public function configure(states:Array):void
		{
			for each (var stateData:Object in states)
			{
				var state:IState = createState(stateData);
				var name:String = stateData.name;
				if (!name) throw new ArgumentError("Missing required attribute: 'name'");
				var s:IState = registerState(name, state);
				for each (var transitionData:Object in stateData.transitions)
				{
					var t:Transition = transitionData.after ?
						s.after(transitionData.after) :
						s.onEvent(transitionData.event, transitionData.dispatcher);
					if (transitionData.action)
						t.action(transitionData.action);
					else if (transitionData.goto)
						t.goto(transitionData.goto);
				}
				if (stateData.nested)
					(s as FSM).configure(stateData.nested);
			}
		}
	}
}
